/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the CometMI Project.
 */
package org.cometmi.gwt.server.impl;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.CtPrimitiveType;
import javassist.NotFoundException;

import org.apache.log4j.Logger;
import org.cometmi.gwt.server.util.ServerUtil;
import org.cometmi.gwt.shared.MiProxy;
import org.cometmi.gwt.shared.annotations.DeclaredConstructor;
import org.cometmi.gwt.shared.annotations.TargetMiObjectClass;
import org.cometmi.gwt.shared.request.var.ReturnVar;
import org.cometmi.gwt.shared.util.ProxyBase;
import org.cometmi.gwt.shared.util.ProxyFactory;

public class ProxyFactoryServer implements ProxyFactory
{
	static protected Logger logger = Logger.getLogger(ProxyFactoryServer.class);

	private HashMap<String, Class<? extends MiProxy>> classMap;
	private ClassPool classPool;

	public ProxyFactoryServer() throws NotFoundException
	{
		this.classMap = new HashMap<String, Class<? extends MiProxy>>();

		ClassPool parent = ClassPool.getDefault();
		this.classPool = new ClassPool(parent);
		this.classPool.insertClassPath(new ClassClassPath(getClass()));
	}

	@Override
	public ProxyBase createProxy(Class<? extends MiProxy> proxyType)
	{
		/* Adding the required class to the pool class path. */
		this.classPool.insertClassPath(new ClassClassPath(proxyType));

		String packageName = ProxyFactoryServer.class.getPackage().getName();
		String proxyClassName = packageName + "." + proxyType.getSimpleName() + "___Impl";

		try
		{
			if (this.classMap.containsKey(proxyClassName))
				return (ProxyBase) this.classMap.get(proxyClassName).newInstance();

			DeclaredConstructor a = proxyType.getAnnotation(DeclaredConstructor.class);
			TargetMiObjectClass b = proxyType.getAnnotation(TargetMiObjectClass.class);

			CtClass interf = this.classPool.get(proxyType.getName());
			CtClass base = this.classPool.get(ProxyBase.class.getName());

			CtClass clazz = this.classPool.makeClass(proxyClassName, base);
			clazz.addInterface(interf);

			/* Defining the constructor */
			StringBuilder ctorBody = new StringBuilder();
			ctorBody.append("{\n");

			/*
			 * We don't make any validation here since the same validation is
			 * already made on client, and client will not compile if it's not
			 * valid.
			 */
			if (b != null)
				ctorBody.append("	super.targetMiObjectClass = ").append(b.miObjectClass().getCanonicalName())
						.append(".class;\n");

			StringBuilder ctorSignature = new StringBuilder();

			if (a != null) for (Class<?> t : a.paramTypes())
				ctorSignature.append(ServerUtil.getJNISignature(t));

			ctorBody.append("	super.constrSignature = \"").append(ctorSignature).append("\";\n");

			ctorBody.append("}");

			CtConstructor constr = new CtConstructor(new CtClass[0], clazz);
			constr.setBody(ctorBody.toString());

			clazz.addConstructor(constr);

			for (CtMethod m : interf.getDeclaredMethods())
			{
				String methodName = m.getName();

				StringBuilder signature = new StringBuilder();

				/*
				 * Workaround to retrieve ReturnVar generic type, required to
				 * generate the method signature.
				 */
				CtClass[] paramTypes = m.getParameterTypes();
				Class<?>[] _paramTypes = new Class<?>[paramTypes.length];

				for (int i = 0; i < paramTypes.length; i++)
					if (paramTypes[i] instanceof CtPrimitiveType)
					{
						CtPrimitiveType prim = (CtPrimitiveType) paramTypes[i];
						_paramTypes[i] = ServerUtil.getType(prim.getDescriptor());
					}
					else
						_paramTypes[i] = Class.forName(paramTypes[i].getName());

				Method _m = proxyType.getMethod(m.getName(), _paramTypes);

				for (Type t : _m.getGenericParameterTypes())
					if (t instanceof ParameterizedType && ReturnVar.class.isAssignableFrom((Class<?>) t))
					{
						ParameterizedType type = (ParameterizedType) t;
						signature.append(ServerUtil.getJNISignature((Class<?>) type.getActualTypeArguments()[0]));
					}
					else if (t instanceof Class && MiProxy.class.isAssignableFrom((Class<?>) t))
					{
						/*
						 * If the argument is a proxy, for the signature must be
						 * used the target object type.
						 */
						Class<?> p = (Class<?>) t;
						TargetMiObjectClass anno = p.getAnnotation(TargetMiObjectClass.class);

						if (anno != null)
							signature.append(ServerUtil.getJNISignature(anno.miObjectClass()));
						else
							signature.append(ServerUtil.getJNISignature((Class<?>) t));
					}
					else
						signature.append(ServerUtil.getJNISignature((Class<?>) t));

				CtMethod method = CtNewMethod.copy(m, methodName, clazz, null);

				CtClass returnType = method.getReturnType();
				boolean returnVoid = returnType.equals(CtClass.voidType);

				StringBuilder body = new StringBuilder();
				body.append("{\n");
				body.append(returnVoid ? "	$proceed(" : "	return $proceed(");
				body.append("super.objectId, ");
				body.append("super.targetMiObjectClass, ");
				body.append("\"").append(method.getName()).append("\", ");
				body.append("\"").append(signature).append("\", ");

				if (returnVoid)
					body.append("$args);");
				else
					body.append("$args, ").append(returnType.getName()).append(".class);");

				body.append("}");

				method.setBody(body.toString(), "super.manager", "publishMethod");

				clazz.addMethod(method);
			}

			clazz.writeFile();

			// FIXME ProtectionDomain could be required?

			@SuppressWarnings("unchecked")
			Class<? extends MiProxy> jClazz = this.classPool.toClass(clazz);

			this.classMap.put(proxyClassName, jClazz);

			return (ProxyBase) jClazz.newInstance();
		}
		catch (Exception e)
		{
			logger.error("[ProxyFactoryServer] Impossible to create a proxy of type " + proxyType, e);
			return null;
		}
	}
}
